/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.eclipse.ui.preferences.br;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.dialogs.ContainerCheckedTreeViewer;

import net.sourceforge.pmd.eclipse.runtime.preferences.IPreferences;
import net.sourceforge.pmd.eclipse.ui.AbstractColumnDescriptor;
import net.sourceforge.pmd.eclipse.ui.ColumnDescriptor;
import net.sourceforge.pmd.eclipse.ui.ModifyListener;
import net.sourceforge.pmd.eclipse.ui.PMDUiConstants;
import net.sourceforge.pmd.eclipse.ui.nls.StringKeys;
import net.sourceforge.pmd.eclipse.ui.preferences.editors.SWTUtil;
import net.sourceforge.pmd.eclipse.ui.views.ChangeRecord;
import net.sourceforge.pmd.eclipse.util.ResourceManager;

/**
 * Tree table support, everything non-Rule related.
 * 
 * @author Brian Remedios
 */
public abstract class AbstractTreeTableManager<T extends Object> extends AbstractTableManager<T> {

    protected ContainerCheckedTreeViewer treeViewer;

    private Button selectAllButton;
    private Button unSelectAllButton;
    private ModifyListener modifyListener;
    private Label activeCountLabel;
    private Label activeCountIssue;

    private ChangeRecord<T> changes;

    private Map<Integer, List<Listener>> paintListeners = new HashMap<>();

    public AbstractTreeTableManager(String theWidgetId, IPreferences thePreferences, ColumnDescriptor[] theColumns) {
        super(theWidgetId, thePreferences, theColumns);
    }

    protected static ColumnWidthAdapter adapterFor(final TreeColumn column) {
        return new ColumnWidthAdapter() {
            @Override
            public int width() {
                return column.getWidth();
            }

            @Override
            public void width(int newWidth) {
                column.setWidth(newWidth);
            }

            @Override
            public Display display() {
                return column.getDisplay();
            }

            @Override
            public void setData(String key, Object value) {
                column.setData(key, value);
            }

            @Override
            public Object getData(String key) {
                return column.getData(key);
            }
        };
    }

    public Tree getControl() {
        return treeViewer.getTree();
    }

    @Override
    protected String idFor(Object column) {
        return ((TreeColumn) column).getToolTipText();
    }

    protected void removed(Collection<T> items) {
        if (changes == null) {
            changes = new ChangeRecord<>();
        }
        changes.removed(items);
        updateCheckControls();
    }

    protected void added(T item) {
        if (changes == null) {
            changes = new ChangeRecord<>();
        }
        changes.added(item);
        updateCheckControls();
    }

    protected Map<Integer, List<Listener>> paintListeners() {
        return paintListeners;
    }

    protected void createCheckBoxColumn(Tree tree) {
        TreeColumn tc = new TreeColumn(tree, 0);

        final AtomicReference<Point> textExtent = new AtomicReference<>();
        Display.getDefault().syncExec(new Runnable() {
            @Override
            public void run() {
                Image image = new Image(Display.getDefault(), 15, 15);
                GC gc = new GC(image);
                textExtent.set(gc.textExtent("m"));
                gc.dispose();
                image.dispose();
            }
        });

        tc.setWidth(textExtent.get().x * 4);

        tc.setResizable(true);
        tc.pack();

        tc.addListener(SWT.Selection, new Listener() {
            @Override
            public void handleEvent(Event e) {
                sortByCheckedItems();
            }
        });
    }

    /**
     * Remove all rows, columns, and column painters in preparation for new
     * columns.
     *
     * @return Tree
     */
    protected Tree cleanupRuleTree() {

        Tree tree = treeViewer.getTree();

        tree.clearAll(true);
        while (tree.getColumns().length > 0) {
            // TODO also dispose any heading icons?
            tree.getColumns()[0].dispose();
        }

        // ensure we don't have any previous per-column painters left over
        for (Map.Entry<Integer, List<Listener>> entry : paintListeners.entrySet()) {
            int eventCode = entry.getKey().intValue();
            List<Listener> listeners = entry.getValue();
            for (Listener listener : listeners) {
                tree.removeListener(eventCode, listener);
            }
            listeners.clear();
        }

        return tree;
    }

    protected abstract boolean isQualifiedItem(Object item);

    @Override
    protected abstract void saveItemSelections();

    public int activeItemCount() {

        Object[] checkedItems = treeViewer.getCheckedElements();
        int count = 0;

        for (Object item : checkedItems) {
            if (isQualifiedItem(item)) {
                count++;
            }
        }

        return count;
    }

    protected CheckboxTreeViewer treeViewer() {
        return treeViewer;
    }

    public ChangeRecord<T> changes() {
        return changes;
    }

    protected Button newImageButton(Composite parent, String imageId, String toolTipId) {

        Button button = new Button(parent, SWT.PUSH | SWT.LEFT);
        button.setImage(ResourceManager.imageFor(imageId));
        button.setToolTipText(getMessage(toolTipId));
        button.setEnabled(true);
        return button;
    }

    /**
     *
     * @param parent
     *            Composite
     * @return Button
     */
    protected Button buildSelectAllButton(Composite parent) {

        Button button = newImageButton(parent, PMDUiConstants.ICON_BUTTON_CHECK_ALL,
                StringKeys.PREF_RULESET_BUTTON_CHECK_ALL);
        button.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent event) {
                setAllItemsActive();
            }
        });

        return button;
    }

    /**
     *
     * @param parent
     *            Composite
     * @return Button
     */
    protected Button buildUnselectAllButton(Composite parent) {

        Button button = newImageButton(parent, PMDUiConstants.ICON_BUTTON_UNCHECK_ALL,
                StringKeys.PREF_RULESET_BUTTON_UNCHECK_ALL);
        button.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent event) {
                preferences.getActiveRuleNames().clear();
                treeViewer().setCheckedElements(new Object[0]);
                setModified();
                updateCheckControls();
            }
        });

        return button;
    }

    protected abstract String nameFor(Object treeItemData);

    /**
     * @param item
     *            TreeItem
     * @param checked
     *            boolean
     */
    private void check(TreeItem item, boolean checked) {

        item.setChecked(checked);
        Object itemData = item.getData();
        if (itemData == null || itemData instanceof RuleGroup) {
            return;
        }

        String name = nameFor(itemData);

        isActive(name, checked);

        updateCheckControls();
        setModified();
    }

    protected abstract void selectedItems(Object[] items);

    protected abstract void updateTooltipFor(TreeItem item, int columnIndex);

    // TODO move to util
    public static int columnIndexAt(TreeItem item, int xPosition) {

        TreeColumn[] cols = item.getParent().getColumns();
        Rectangle bounds;

        for (int i = 0; i < cols.length; i++) {
            bounds = item.getBounds(i);
            if (bounds.x < xPosition && xPosition < (bounds.x + bounds.width)) {
                return i;
            }
        }
        return -1;
    }

    protected void buildTreeViewer(Composite parent) {

        int treeStyle = SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL | SWT.MULTI | SWT.FULL_SELECTION | SWT.CHECK;
        treeViewer = new ContainerCheckedTreeViewer(parent, treeStyle);

        final Tree tree = treeViewer.getTree();
        tree.setLinesVisible(true);
        tree.setHeaderVisible(true);

        treeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
            @Override
            public void selectionChanged(SelectionChangedEvent event) {
                IStructuredSelection selection = (IStructuredSelection) event.getSelection();
                selectedItems(selection.toArray());
            }
        });

        addDeleteListener(tree);

        tree.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent ev) {
                if (ev.character == ' ') {
                    toggleSelectedItems();
                }
            }
        });

        tree.addListener(SWT.Selection, new Listener() {
            @Override
            public void handleEvent(Event event) {
                if (event.detail == SWT.CHECK) {
                    TreeItem item = (TreeItem) event.item;
                    boolean checked = item.getChecked();
                    checkItems(item, checked);
                    checkPath(item.getParentItem(), checked, false);
                }
                // if (!checkedRules.isEmpty())
                // System.out.println(checkedRules.iterator().next());
            }
        });

        tree.addListener(SWT.MouseMove, new Listener() {
            @Override
            public void handleEvent(Event event) {
                Point point = new Point(event.x, event.y);
                TreeItem item = tree.getItem(point);
                if (item != null) {
                    int columnIndex = columnIndexAt(item, event.x);
                    updateTooltipFor(item, columnIndex);
                }
            }
        });

        setupMenusFor(tree);
    }

    @Override
    protected int headerHeightFor(Control control) {
        return ((Tree) control).getHeaderHeight();
    }

    @Override
    protected void setMenu(Control control, Menu menu) {
        ((Tree) control).setMenu(menu);
    }

    @Override
    protected Rectangle clientAreaFor(Control control) {
        return ((Tree) control).getClientArea();
    }

    public void updated(Object item) {
        treeViewer.update(item, null);
    }

    /**
     * Method checkPath.
     * 
     * @param item
     *            TreeItem
     * @param checked
     *            boolean
     * @param grayed
     *            boolean
     */
    protected void checkPath(TreeItem item, boolean checked, boolean grayed) {
        if (item == null) {
            return;
        }
        if (grayed) {
            checked = true;
        } else {
            int index = 0;
            TreeItem[] items = item.getItems();
            while (index < items.length) {
                TreeItem child = items[index];
                if (child.getGrayed() || checked != child.getChecked()) {
                    checked = true;
                    grayed = true;
                    break;
                }
                index++;
            }
        }
        check(item, checked);
        item.setGrayed(grayed);
        checkPath(item.getParentItem(), checked, grayed);
    }

    /**
     * @param item
     *            TreeItem
     * @param checked
     *            boolean
     */
    protected void checkItems(TreeItem item, boolean checked) {
        item.setGrayed(false);
        check(item, checked);
        TreeItem[] items = item.getItems();
        for (TreeItem item2 : items) {
            checkItems(item2, checked);
        }
        updateCheckControls();
    }

    private void toggleSelectedItems() {
        // TODO
        System.out.println("TODO: toggle selected items");
    }

    protected void buildActiveCountWidgets(Composite parent) {
        activeCountLabel = new Label(parent, 0);
        activeCountLabel.setText("---");

        GridData data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER, GridData.CENTER, true, false, 1, 1);
        activeCountLabel.setAlignment(SWT.RIGHT);
        activeCountLabel.setLayoutData(data);

        activeCountIssue = new Label(parent, 0);
        data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER, GridData.CENTER, true, false, 1, 1);
        activeCountIssue.setAlignment(SWT.RIGHT);
        activeCountIssue.setLayoutData(data);
    }

    protected void activeCountDetails(String msg, Image image) {

        if (activeCountLabel == null) {
            return;
        }

        activeCountLabel.setText(msg);
        activeCountIssue.setImage(image);
        activeCountIssue.getParent().pack();
        activeCountLabel.getParent().pack(); // handle changing string length
    }

    protected void buildCheckButtons(Composite parent) {

        selectAllButton = buildSelectAllButton(parent);
        unSelectAllButton = buildUnselectAllButton(parent);
    }

    @Override
    protected ColumnWidthAdapter columnAdapterFor(ColumnDescriptor desc) {
        TreeColumn column = columnFor(desc);
        return adapterFor(column);
    }

    protected void redrawTable() {
        redrawTable("-", -1);
    }

    @Override
    protected void redrawTable(String sortColumnLabel, int sortDir) {
        TreeColumn sortColumn = columnFor(sortColumnLabel);
        treeViewer().getTree().setSortColumn(sortColumn);
        treeViewer().getTree().setSortDirection(sortDir);
    }

    private TreeColumn columnFor(String tooltipText) {
        for (TreeColumn column : treeViewer().getTree().getColumns()) {
            if (String.valueOf(column.getToolTipText()).equals(tooltipText)) {
                return column;
            }
        }
        return null;
    }

    private TreeColumn columnFor(ColumnDescriptor desc) {
        for (TreeColumn column : treeViewer().getTree().getColumns()) {
            if (Objects.equals(column.getData(AbstractColumnDescriptor.DESCRIPTOR_KEY), desc)) {
                return column;
            }
        }
        return null;
    }

    protected void formatValueOn(StringBuilder target, Object value, Class<?> datatype) {

        ValueFormatter formatter = FormatManager.formatterFor(datatype);
        if (formatter != null) {
            formatter.format(value, target);
            return;
        }

        target.append(value); // should not get here..breakpoint here
    }

    // protected Button buildSortByCheckedItemsButton(Composite parent) {
    // Button button = new Button(parent, SWT.PUSH | SWT.LEFT);
    // button.setToolTipText("Sort by checked items");
    // button.setImage(ResourceManager.imageFor(PMDUiConstants.ICON_BUTTON_SORT_CHECKED));
    //
    // button.addSelectionListener(new SelectionAdapter() {
    // public void widgetSelected(SelectionEvent event) {
    // sortByCheckedItems();
    // }
    // });
    //
    // return button;
    // }

    protected abstract void setAllItemsActive();

    protected abstract void sortByCheckedItems();

    public void modifyListener(ModifyListener theListener) {
        modifyListener = theListener;
    }

    protected void setModified() {
        if (modifyListener != null) {
            modifyListener.setModified();
        }
    }

    protected void updateButtonsFor(int selections, int totalSelections) {

        SWTUtil.setEnabled(selectAllButton, selections < totalSelections);
        SWTUtil.setEnabled(unSelectAllButton, selections > 0);
    }

    protected abstract void updateCheckControls();

    /**
     * Refresh the list
     */
    protected void refresh() {
        try {
            treeViewer().getControl().setRedraw(false);
            treeViewer().refresh();
        } catch (ClassCastException e) {
            plugin.logError("Ignoring exception while refreshing table", e);
        } finally {
            treeViewer().getControl().setRedraw(true);
        }
    }

}
